package com.benromberg.junit.threadconductor;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class ThreadConductorRule implements TestRule {
	private Thread mainThread = Thread.currentThread();
	private List<ConductedThread> conductedThreads = new ArrayList<ConductedThread>();

	@Override
	public Statement apply(final Statement base, final Description description) {
		return new Statement() {
			@Override
			public void evaluate() throws Throwable {
				base.evaluate();
				for (ConductedThread conductedThread : conductedThreads) {
					conductedThread.closeThread();
				}
			}
		};
	}

	public ConductedThread createThread(Runnable runnable) {
		ConductedThread conductedThread = new ConductedThread(runnable);
		conductedThreads.add(conductedThread);
		return conductedThread;
	}

	public class ConductedThread {
		private Thread thread;
		private RunnableWrapper wrapper;

		public ConductedThread(final Runnable clientRunnable) {
			wrapper = new RunnableWrapper();
			Runnable runnable = new Runnable() {
				@Override
				public void run() {
					System.out.println("created thread " + Thread.currentThread().getId());
					clientRunnable.run();
					mainThread.interrupt();
					while (true) {
						synchronized (this) {
							try {
								wait();
								throw new RuntimeException(new TimeoutException());
							} catch (InterruptedException e) {
							}
						}
						if (wrapper.getRunnable() != null) {
							wrapper.getRunnable().run();
						} else {
							break;
						}
						mainThread.interrupt();
					}
					System.out.println("closing thread " + Thread.currentThread().getId());
				}
			};
			thread = new Thread(runnable);
			thread.start();
			waitForThread();
		}

		private synchronized void waitForThread() {
			try {
				wait();
				throw new RuntimeException(new TimeoutException());
			} catch (InterruptedException e) {
			}
		}

		public void continueThread(Runnable runnable) {
			wrapper.setRunnable(runnable);
			thread.interrupt();
			waitForThread();
		}

		public void closeThread() {
			wrapper.setRunnable(null);
			thread.interrupt();
		}

		public class RunnableWrapper {
			private Runnable runnable;

			public void setRunnable(Runnable runnable) {
				this.runnable = runnable;
			}

			public Runnable getRunnable() {
				return runnable;
			}
		}
	}

}
